package zhuzx.generator;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.zhuzx.util.GeneralUtil;
import org.zhuzx.util.JdbcUtil;
import org.zhuzx.util.MicroUtil;
import org.zhuzx.util.UnifiedConstants;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author zhuzx 2022/8/6
 */
@Slf4j
public class DatabaseMetadataUtil {

	/**
	 * 数据类型映射：<br>
	 * 以DatabaseMetaData.getColumns.getString得到的DATA_TYPE值为键。<br>
	 * 以对应的Java对象类型名字为值：若为lang包下的，则不带包名，否则就是全类名（比如math包下的BigDecimal）。<br>
	 * 若DATA_TYPE的类型比较特殊，暂不知道用什么类型去对应的，则给个“UNKNOWN_TYPE”作为标识等待人工修正。
	 */
	static final Map<Integer,String> types;

	static {
		Map<String,String> originTypes = GeneralUtil.loadPropsFileFromClassDirectory(
				SqlTextParser.class, "ColumnTypeToJavaType.properties");
		Field[] fields = Types.class.getFields();
		types = new HashMap<>();
		for (Field field : fields) {
			try {
				types.put(Integer.parseInt(field.get(null).toString()),
						GeneralUtil.getNotBlankString(originTypes.get(field.getName()), "UNKNOWN_TYPE"));
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}
	
	public static void main(String[] args) {
		Connection conn = JdbcUtil.getDbConn(null);
		getColumns(conn, "n_event_general_view_d_d");
		JdbcUtil.close(conn);
	}

	/**
	 * 获取指定表的元数据<br>
	 * 用于取代老的getMetaData方法（返回二维数组的那个）
	 */
	public static List<DbColumnMetadata> getColumns(Connection conn, String tableName) {
		/*
		 * 从oracle获取的DatabaseMetaData有严重的问题。
		 * 其getColumns方法得到的ResultSet中有重复的列信息（结果集有重复的行）。
		 * 关键是这些重复的列还不是全部的，并不是把所有的列都重复一遍，暂时还未找到规律，哪些列会重复哪些列不会。
		 * 而且重复的那一部分列信息没有注释，推断是jdbc驱动的问题。
		 * 鉴于以上原因，需要通过ResultSetMetaData.getColumnCount()获取字段数量。
		 * 实测DatabaseMetaData.getColumns.getString的输出：
		 * TABLE_CAT: proddb（即数据库schema名字）
		 * DATA_TYPE: 12
		 * TYPE_NAME: VARCHAR
		 * IS_NULLABLE: NO
		 */
		try {
			DatabaseMetaData dmd = conn.getMetaData();
			ResultSet rs = dmd.getColumns(conn.getCatalog(), null, tableName ,null);
			if (!rs.next()) {
				rs.close();
				if (!MicroUtil.isUpperCase(tableName)) {//换大写重试一次
					return getColumns(conn, tableName.toUpperCase());
				} else {
					throw new RuntimeException("无法获取到" + tableName + "的元数据");
				}
			}
//			testOutputColumnsMetadata(rs);
			List<DbColumnMetadata> columns = new ArrayList<>();
			Map<String, List<String>> uniqueIndexFields = getUniqueIndexFields(conn, tableName);
			do {
				DbColumnMetadata column = new DbColumnMetadata();
				column.oriName = rs.getString("COLUMN_NAME");
				column.oriType = rs.getString("TYPE_NAME");
				column.javaName = MicroUtil.convertUnderlineToCamelCased(column.oriName);
				column.javaType = types.get(rs.getInt("DATA_TYPE"));
				column.remark = rs.getString("REMARKS");
				column.isNullable = rs.getString("IS_NULLABLE").equals("YES");
				column.isAutoincr = rs.getString("IS_AUTOINCREMENT").equals("YES");
				column.ordinal = rs.getInt("ORDINAL_POSITION");
				column.defaultValue = rs.getString("COLUMN_DEF");
				column.size = rs.getInt("COLUMN_SIZE");
				column.decimalDigits = rs.getInt("DECIMAL_DIGITS");
				uniqueIndexFields.forEach((key, value) -> {
					//如果唯一索引Map中有这个列，则把对应的字段信息挂到这个字段下。
					if (value.contains(column.oriName)) {
						column.uniqueColumns = value;
					}
				});
				columns.add(column);
			} while (rs.next());
			Map<String, String> oriNameMappingRemark = columns.stream().collect(Collectors.toMap(
					DbColumnMetadata::getOriName, DbColumnMetadata::getRemark));
			columns.forEach(column -> {
				if (column.uniqueColumns != null) {
					column.uniqueColumnRemarks = column.uniqueColumns.stream().map(oriNameMappingRemark::get).collect(Collectors.toList());
				}
			});
			return columns;
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * @return 索引名为键，对应的字段列表为值。
	 */
	static Map<String,List<String>> getUniqueIndexFields(Connection connection, String tableName) {
		try {
			DatabaseMetaData dbMetaData = connection.getMetaData();
			ResultSet rs = dbMetaData.getIndexInfo(null, null, tableName, true, false);
			Map<String,List<String>> fields = new LinkedHashMap<>();
			while (rs.next()) {
				String indexName = rs.getString("INDEX_NAME");
				if (indexName.equals("PRIMARY")) {
					continue;
				}
				List<String> list = fields.computeIfAbsent(indexName, k -> new ArrayList<>(1));
				list.add(rs.getString("COLUMN_NAME"));
			}
			return fields;
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
	
	@SuppressWarnings("unused")
	private static void testOutputColumnsMetadata(ResultSet rs) {
		try {
			ResultSetMetaData metaData = rs.getMetaData();
			int columnCount = metaData.getColumnCount();
			List<String> columnNames = new ArrayList<>();
			for (int i = 1; i <= columnCount; i++) {
				columnNames.add("[" + MicroUtil.subBehind(metaData.getColumnClassName(i), '.') + "]\t"
						+ metaData.getColumnName(i));
			}
			do {
				columnNames.forEach(item -> {
					try {
						System.out.println(item + "：" + rs.getString(MicroUtil.subBehind(item, ']').trim()));
					} catch (SQLException e) {
						throw new RuntimeException(e);
					}
				});
				System.out.println("----");
			} while (rs.next());
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Getter
	@AllArgsConstructor
	public static class EnumDefinition {
		/** 整型枚举值 */
		int value;
		/** 除去枚举值的纯描述部分，用于前端展示。*/
		String name;
		/** 枚举项的英文标识（不一定准确而且可能为null）*/
		String engCode;
		/** 枚举项的完整描述文本，可能包括“xx状态”的前缀、枚举值、中文描述、英文标识，可作为常量上的注释。*/
		String fullDesc;
	}

	/**
	 * 解析数据库枚举值字段注释，得到注释中列举的枚举项对应的一组枚举定义对象。<br>
	 * 注释中需要用标点分隔各项枚举的说明，并且保证每段枚举说明都以整型的枚举值开头，才能提取出枚举值、描述、英文标识。<br>
	 * 若注释是以一个纯中文描述开头，则将它视为字段含义描述，每个fullDesc都会以它开头。<br>
	 * 枚举值与说明文本之间可以使用-作为分隔符，会在提取时自动剔除（若使用了其它分隔符的不会做处理），也可以直接紧凑连接。<br>
	 * 注释举例：<br>
	 * 卡片状态：0未操作；1已操作；2已被系统忽略；-1内部处理失败。<br>
	 * 审批状态：1-PENDING-审批中，2-APPROVED-通过，3-REJECTED-拒绝，4-CANCELED-撤回。<br>
	 * 1图片image、2附件attachment
	 */
	static List<EnumDefinition> parseEnumComment(String comment) {
		String oriComment = comment;
		comment = comment.replaceAll("\\s+", "");
		//取描述字段含义的“xx状态”几个字加横杠作为生成的常量注释的前缀
		String sepRegex = UnifiedConstants.REGEX_PUNCTUATION_SEPARATION;
		String fieldDescPrefix = MicroUtil.subBeforeByRegex(comment, sepRegex);
		//若第一段文本不是纯中文，则不是前缀。
		if (fieldDescPrefix.equals(comment) || !fieldDescPrefix.matches(UnifiedConstants.REGEX_CHINESE_TEXT)) {
			fieldDescPrefix = "";
		} else {
			fieldDescPrefix += "-";
			//去掉“xx状态”加第一个标点长度的前缀部分，再从后面的文本中分割出枚举项。
			comment = comment.substring(fieldDescPrefix.length());
		}
		for (int i = 1; i < sepRegex.length() - 1; i++) {//去掉结尾的标点符号
			comment = MicroUtil.removeSuffix(comment, String.valueOf(sepRegex.charAt(i)));
		}
		//每段枚举说明之间可能会用断句标点或者顿号分隔
		String[] enums = comment.split(sepRegex + "|、");
		if (enums.length == 1) {
			log.info("注释文本【{}】中的枚举项不足，无法提取枚举定义。", oriComment);
			return null;
		}
		List<EnumDefinition> defs = new ArrayList<>(enums.length);
		for (String item : enums) {
			//整型的枚举值
			String intValue = MicroUtil.extractFirst(item, "-?\\d+");
			if (intValue == null) {
				log.info("注释文本【{}】中的枚举项【{}】不是以整型开头，无法提取枚举定义。", oriComment, item);
				return null;
			}
			//除去枚举值的纯描述部分
			String desc = MicroUtil.removePrefix(item, intValue);
			desc = MicroUtil.removePrefix(desc, "-");
			//从纯描述部分中寻找可能的英文标识
			String engCode = MicroUtil.extractFirst(desc, "\\w{3,}");
			defs.add(new EnumDefinition(Integer.parseInt(intValue), desc, engCode, fieldDescPrefix + item));
		}
		//若没有提取到英文标识，并且只有两个枚举项的，很有可能是二元字段。
		if (defs.stream().allMatch(item -> item.getEngCode() == null) && defs.size() == 2) {
			if (oriComment.startsWith("是否") || (defs.get(0).name.equals("是") && defs.get(1).name.equals("否"))) {
				if (defs.get(0).value == UnifiedConstants.FLAG_AFFIRMATIVE && defs.get(1).value == UnifiedConstants.FLAG_NEGATIVE) {
					log.info("注释文本【{}】中的枚举项是标准的二元布尔枚举，不需要生成枚举定义。", oriComment);
					return null;
				} else {
					defs.get(0).engCode = "yes";
					defs.get(1).engCode = "no";
				}
			}
		}
		return defs;
	}

	/**
	 * 给枚举型字段生成枚举常量定义代码片段
	 * @param fieldName java字段名
	 * @param comment 字段注释
	 * @return 一组带注释的public static final int常量定义代码
	 */
	static String generateEnumConstantsCode(String fieldName, String comment, String javaType) {
		List<EnumDefinition> defs = parseEnumComment(comment);
		if (GeneralUtil.isEmpty(defs)) {
			return null;
		}
		String primitiveType = javaType.equals("Byte") ? "byte" : "int";
		if (defs.size() >= 5 && defs.stream().allMatch(item -> item.engCode != null)) {
			return generateEnumClassCode(fieldName, defs, primitiveType);
		}
		StringBuilder sb = new StringBuilder();
		defs.forEach(item -> {
			sb.append("/** ").append(item.fullDesc).append(" */\n");
			sb.append("public static final ").append(primitiveType).append(" ").append(
					(MicroUtil.convertCamelCasedToUnderline(fieldName) + "_" +
							GeneralUtil.getNotBlankString(item.engCode, "")).toUpperCase()
			).append(" = ").append(item.value).append(";\n");
		});
		sb.deleteCharAt(sb.length() - 1);
		return sb.toString();
	}
	
	static String generateEnumClassCode(String fieldName, List<EnumDefinition> defs, String primitiveType) {
		Map<String, Object> data = new HashMap<>(Collections.singletonMap("defs", defs));
		data.put("fieldName", fieldName);
		data.put("primitiveType", primitiveType);
		return FreemarkerUtil.generate("enum.java.ftl", data);
	}
	
}